package br.mikhas.util.date;

import static br.mikhas.util.Numbers.min;

import java.util.TimeZone;

/**
 * Immutable object which represents a time wihout regional data.
 * <p>
 * The time stores the hour, minutes, seconds and milliseconds of an instant.
 * 
 * @author Mikhail Domanoski
 * 
 */
public final class Time implements Instant, Comparable<Time> {

	private static final long MILLIS_IN_HOUR = 1000 * 60 * 60;

	/**
	 * The instant this time represents
	 */
	private final long timestamp;

	/**
	 * The hour
	 */
	private int hour = -1;

	/**
	 * THe minutes
	 */
	private int minutes = -1;

	/**
	 * The seconds
	 */
	private int seconds = -1;

	/**
	 * The milliseconds
	 */
	private int millis = -1;

	/**
	 * Cached string representation of the time
	 */
	private String stringed;

	/**
	 * The partial fields
	 */
	private int[] fields = new int[7];

	/**
	 * Creates a new instance of Time determinating the hour.
	 * 
	 * @param hour
	 *            The hour the time represents
	 */
	public Time(int hour) {
		this(hour, 0, 0, 0);
	}

	/**
	 * Creates a new instance of Time determinating the hour and minutes.
	 * 
	 * @param hour
	 *            The hour the time represents
	 * @param minutes
	 *            The minutes the time represents
	 */
	public Time(int hour, int minutes) {
		this(hour, minutes, 0, 0);
	}

	/**
	 * Creates a new instance of Time determinating the hour, minutes and
	 * seconds it represents.
	 * 
	 * @param hour
	 *            The hour the time represents
	 * @param minutes
	 *            The minutes the time represents
	 * @param seconds
	 *            The seconds the time represents
	 */
	public Time(int hour, int minutes, int seconds) {
		this(hour, minutes, seconds, 0);
	}

	/**
	 * Creates a new instance of time determinating each one of it's components.
	 * 
	 * @param hour
	 *            The hour the time represents
	 * @param minutes
	 *            The minutes the time represents
	 * @param seconds
	 *            The seconds the time represents
	 * @param millis
	 *            The milliseconds the time represents
	 */
	public Time(int hour, int minutes, int seconds, int millis) {

		if (min(hour, minutes, seconds, millis) < 0) {
			throw new IllegalArgumentException(
					String.format(
							"Illegal time components [hour:%d, minutes:%d, seconds:%d, milliseconds:%d]",
							hour, minutes, seconds, millis));
		}

		long timestamp = hour * MILLIS_IN_HOUR;
		timestamp += minutes * (1000 * 60);
		timestamp += seconds * (1000);
		timestamp += millis;
		this.timestamp = timestamp;
		denormalize(timestamp);
	}

	/**
	 * Creates a new instance of Time using the current system time;
	 */
	public Time() {
		long millis = System.currentTimeMillis();
		int offset = TimeZone.getDefault().getOffset(millis);

		this.denormalize(millis + offset);

		long timestamp = this.hour * MILLIS_IN_HOUR;
		timestamp += this.minutes * (1000 * 60);
		timestamp += this.seconds * (1000);
		timestamp += this.millis;

		this.timestamp = timestamp;
	}

	/**
	 * Creates a new instance of Time using a providen instant
	 * 
	 * @param instant
	 *            The instant the time will represents.
	 */
	public Time(long instant) {
		this.denormalize(instant);

		long timestamp = this.hour * MILLIS_IN_HOUR;
		timestamp += this.minutes * (1000 * 60);
		timestamp += this.seconds * (1000);
		timestamp += this.millis;

		this.timestamp = timestamp;
	}

	/**
	 * Extract and populates the fields of the Time using a base instant.
	 * 
	 * @param millis
	 *            The instant to calculate the components of the time
	 */
	private void denormalize(long millis) {
		this.hour = DateCalculator.getHour(millis);
		this.minutes = DateCalculator.getMinutes(millis);
		this.seconds = DateCalculator.getSeconds(millis);
		this.millis = DateCalculator.getMillis(millis);

		fields[Instant.HOUR] = this.hour;
		fields[Instant.MINUTE] = this.minutes;
		fields[Instant.SECOND] = this.seconds;
		fields[Instant.MILLISECOND] = this.millis;
	}

	/**
	 * Compares to Dates and return an integer.
	 * <p>
	 * It's comprared only the instant of the time.
	 */
	public int compareTo(Time other) {
		return (int) (this.timestamp - other.timestamp);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		} else if (obj == this) {
			return false;
		} else {
			if (obj instanceof Time) {
				Time t = (Time) obj;
				return t.timestamp == this.timestamp;
			} else {
				return false;
			}
		}
	}

	@Override
	public String toString() {
		// 00:00:00:000
		if (this.stringed == null) {
			StringBuilder builder = new StringBuilder(12);
			if (hour < 10) {
				builder.append(0);
			}
			builder.append(hour);
			builder.append(':');
			if (minutes < 10) {
				builder.append(0);
			}
			builder.append(minutes);
			builder.append(':');
			if (seconds < 10) {
				builder.append(0);
			}
			builder.append(seconds);
			builder.append(':');
			if (millis < 100) {
				builder.append(0);
				if (millis < 10) {
					builder.append(0);
				}
			}
			builder.append(millis);
			this.stringed = builder.toString();
		}

		return stringed;
	}

	/**
	 * Sums the current time object with <code>time</code>
	 * 
	 * @param time
	 *            The time to sum with th current time
	 * @return A new {@link Time} object with the sum result
	 */
	public Time sum(Time time) {
		return sum(this, time);
	}

	/**
	 * Executes the sum of two {@link Time} objects and returns it's result.
	 * 
	 * @param a
	 *            The first time object to sum
	 * @param b
	 *            The second time object to sum
	 * @return
	 */
	public static Time sum(Time a, Time b) {
		if (a == null && b == null) {
			return new Time(0L);
		} else if (a == null) {
			return b;
		} else if (b == null) {
			return a;
		} else {
			return new Time(a.timestamp + b.timestamp);
		}
	}

	@Override
	public boolean containsField(int field) {
		return field > Instant.DAY && field <= Instant.MILLISECOND;
	}

	@Override
	public int fieldCount() {
		return 4;
	}

	@Override
	public int getField(int field) {
		if (containsField(field))
			return fields[field];
		else
			throw new IllegalArgumentException("Time does not contains field #"
					+ field);
	}

	@Override
	public int hashCode() {
		return (int) timestamp;
	}
}
